﻿// <copyright file="SqlDatabaseFileWizard.cs" company="T4 Toolbox Team">
//  Copyright © T4 Toolbox Team. All Rights Reserved.
// </copyright>

namespace T4Toolbox.VisualStudio
{
    using System;
    using System.Collections.Generic;
    using System.Configuration;
    using System.IO;
    using System.Linq;
    using System.Reflection;
    using System.Runtime.InteropServices;
    using EnvDTE;
    using Microsoft.CSharp;
    using Microsoft.VisualStudio;
    using Microsoft.VisualStudio.Shell;
    using Microsoft.VisualStudio.Shell.Interop;
    using Microsoft.VisualStudio.TemplateWizard;
    using T4Toolbox.VisualStudio;

    /// <summary>
    /// Creates a SQL Server Database for each file with .mdf extension.
    /// </summary>
    /// <remarks>
    /// This wizard wraps the built-in SqlDatabaseFileWizard and prevents it from 
    /// attempting to create a new database for every file in the template.
    /// </remarks>
    [CLSCompliant(false)]
    public class SqlDatabaseFileWizard : IWizard
    {
        /// <summary>
        /// Web site project kind.
        /// </summary>
        private const string WebSiteProjectKind = "{E24C65DC-7377-472b-9ABA-BC803B73C61A}";

        /// <summary>
        /// Web application project flavor.
        /// </summary>
        private const string WebAppProjectType = "{349c5851-65df-11da-9384-00065b846f21}";

        /// <summary>
        /// Stores the automation object obtained in <see cref="RunStarted"/>.
        /// </summary>
        private object automationObject;

        /// <summary>
        /// Stores a connection string that needs to be added to the .config file.
        /// </summary>
        private ConnectionStringSettings connectionString; 

        /// <summary>
        /// Stores the actual Visual Studio wizard instance used to perform the work.
        /// </summary>
        private IWizard worker;

        /// <summary>
        /// Initializes a new instance of the <see cref="SqlDatabaseFileWizard"/> class.
        /// </summary>
        public SqlDatabaseFileWizard()
        {
            this.worker = new Microsoft.VSDesigner.ProjectWizard.SqlDatabaseFileWizard();
        }

        /// <summary>
        /// Runs custom wizard logic before opening an item in the template.
        /// </summary>
        /// <param name="projectItem">The project item that will be opened.</param>
        public void BeforeOpeningFile(ProjectItem projectItem)
        {
            this.worker.BeforeOpeningFile(projectItem);
        }

        /// <summary>
        /// Runs custom wizard logic when a project has finished generating.
        /// </summary>
        /// <param name="project">The project that finished generating.</param>
        public void ProjectFinishedGenerating(Project project)
        {
            this.worker.ProjectFinishedGenerating(project);
        }

        /// <summary>
        /// Runs custom wizard logic when a project item has finished generating.
        /// </summary>
        /// <param name="projectItem">The project item that finished generating.</param>
        public void ProjectItemFinishedGenerating(ProjectItem projectItem)
        {
            if (projectItem == null)
            {
                throw new ArgumentNullException("projectItem");
            }

            if (Path.GetExtension(projectItem.Name).ToUpperInvariant() == ".MDF")
            {
                // Replace the placeholder file with an actual SQL Server database
                this.worker.ProjectItemFinishedGenerating(projectItem);
                this.AddConnectionStringToConfigFile(projectItem.ContainingProject);
            }
        }

        /// <summary>
        /// Runs custom wizard logic when the wizard has completed all tasks.
        /// </summary>
        public void RunFinished()
        {
            this.worker.RunFinished();
        }

        /// <summary>
        /// Runs custom wizard logic at the beginning of a template wizard run.
        /// </summary>
        /// <param name="automationObject">The automation object being used by the template wizard.</param>
        /// <param name="replacementsDictionary">The list of standard parameters to be replaced.</param>
        /// <param name="runKind">A WizardRunKind indicating the type of wizard run.</param>
        /// <param name="customParams">The custom parameters with which to perform parameter replacement in the project.</param>
        public void RunStarted(object automationObject, Dictionary<string, string> replacementsDictionary, WizardRunKind runKind, object[] customParams)
        {
            this.worker.RunStarted(automationObject, replacementsDictionary, runKind, customParams);

            this.automationObject = automationObject;
#if DEV10
            this.SuppressDataSourceWizard();
#endif
            this.CreateConnectionStrings(new Dictionary<string, string>(replacementsDictionary));
        }

        /// <summary>
        /// Indicates whether the specified project item should be added to the project.
        /// </summary>
        /// <param name="filePath">The path to the project item.</param>
        /// <returns>true if the project item should be added to the project; otherwise, false.</returns>
        public bool ShouldAddProjectItem(string filePath)
        {
            return this.worker.ShouldAddProjectItem(filePath);
        }

        #region implementation

        /// <summary>
        /// Determines if the specified project is a web site.
        /// </summary>
        /// <param name="project">A Visual Studio <see cref="Project"/>.</param>
        /// <returns>true, if the <paramref name="project"/> is a web site, otherwise false.</returns>
        /// <remarks>Implemented based on Microsoft.VSDesigner.Common.DTEUtil.IsWebSiteProject.</remarks>
        private static bool IsWebSiteProject(Project project)
        {
            return project.Kind == WebSiteProjectKind;
        }

        /// <summary>
        /// Replaces parameters in a string.
        /// </summary>
        /// <param name="s">A <see cref="String"/> that contains 0 or more parameters.</param>
        /// <param name="parameters">A <see cref="Dictionary{String, String}"/> object that contains parameter names and values.</param>
        /// <returns>A <see cref="String"/> with parameters replaced.</returns>
        private static string ReplaceParameters(string s, Dictionary<string, string> parameters)
        {
            foreach (KeyValuePair<string, string> parameter in parameters)
            {
                s = s.Replace(parameter.Key, parameter.Value);
            }

            return s;
        }

        /// <summary>
        /// Adds the <see cref="connectionString"/> to the project config file.
        /// </summary>
        /// <param name="project">A <see cref="Project"/> instance.</param>
        private void AddConnectionStringToConfigFile(Project project)
        {
            if (this.connectionString != null)
            {
                // Determine name of the config file
                string fileName = this.IsWebProject(project) ? "Web.config" : "App.Config";
                string fullName = Path.Combine(Path.GetDirectoryName(project.FullName), fileName);

                // Open existing or create new config file
                ExeConfigurationFileMap map = new ExeConfigurationFileMap();
                map.ExeConfigFilename = fullName;
                var configuration = System.Configuration.ConfigurationManager.OpenMappedExeConfiguration(map, ConfigurationUserLevel.None);

                // Add new connection string to the config file 
                configuration.ConnectionStrings.ConnectionStrings.Add(this.connectionString);
                configuration.Save(ConfigurationSaveMode.Modified);

                // Add config file to the project, if necessary
                if (project.DTE.Solution.FindProjectItem(fullName) == null)
                {
                    project.ProjectItems.AddFromFile(fullName);
                }
            }
        }

        /// <summary>
        /// Creates a connection string based on custom parameters specified in the template.
        /// </summary>
        /// <param name="parameters">A <see cref="Dictionary{String, String}"/> object that contains parameter names and values.</param>
        private void CreateConnectionStrings(Dictionary<string, string> parameters)
        {
            // Add standard parameters not yet provided by Visual Studio
            parameters["$fileinputname$"] = Path.GetFileNameWithoutExtension(parameters["$rootname$"]);
            using (CSharpCodeProvider codeProvider = new CSharpCodeProvider())
            {
                parameters["$safeitemname$"] = CSharpTemplate.Identifier(parameters["$fileinputname$"]);
            }

            try
            {
                // Extract connection string parameters
                string connectionString = ReplaceParameters(parameters["$ConnectionString$"], parameters);
                string name             = ReplaceParameters(parameters["$ConnectionStringName$"], parameters);
                string providerName     = ReplaceParameters(parameters["$ConnectionStringProviderName$"], parameters);

                // Create the connection string
                this.connectionString = new ConnectionStringSettings(name, connectionString, providerName);
            }
            catch (KeyNotFoundException)
            {
                // One or more required parameters are missing. Don't create the connection string.
            }
        }

        /// <summary>
        /// Determines if the specified project is a web application.
        /// </summary>
        /// <param name="project">A Visual Studio <see cref="Project"/>.</param>
        /// <returns>true, if the <paramref name="project"/> is a web application, otherwise false.</returns>
        /// <remarks>Implemented based on Microsoft.VSDesigner.Common.DTEUtil.IsWebAppProject.</remarks>
        private bool IsWebAppProject(Project project)
        {
            return this.ProjectHasTargetFlavor(project, new Guid(WebAppProjectType));
        }

        /// <summary>
        /// Determines if the specified project is a web application or a web site.
        /// </summary>
        /// <param name="project">A Visual Studio <see cref="Project"/>.</param>
        /// <returns>true, if the <paramref name="project"/> is a web application or a web site, otherwise false.</returns>
        /// <remarks>Implemented based on Microsoft.VSDesigner.Common.DTEUtil.IsWebProject.</remarks>
        private bool IsWebProject(Project project)
        {
            return IsWebSiteProject(project) || this.IsWebAppProject(project);
        }

        /// <summary>
        /// Returns an <see cref="IVsHierarchy"/> for the specified <paramref name="project"/>.
        /// </summary>
        /// <param name="project">A <see cref="Project"/> instance.</param>
        /// <returns>An <see cref="IVsHierarchy"/> instance.</returns>
        /// <remarks>Implemented based on Microsoft.VSDesigner.Common.DTEUtil.GetIVsHierarchy().</remarks>
        private IVsHierarchy GetIVsHierarchy(Project project)
        {
            using (var serviceProvider = new ServiceProvider(this.automationObject as Microsoft.VisualStudio.OLE.Interop.IServiceProvider))
            {
                IVsSolution solution = (IVsSolution)serviceProvider.GetService(typeof(IVsSolution));
                if (solution == null)
                {
                    return null;
                }

                IVsHierarchy hierarchy;
                Marshal.ThrowExceptionForHR(solution.GetProjectOfUniqueName(project.UniqueName, out hierarchy));
                return hierarchy;
            }
        }

        /// <summary>
        /// Determines if the project has a particular flavor (or sub-type).
        /// </summary>
        /// <param name="project">A <see cref="Project"/> instance.</param>
        /// <param name="targetFlavor">A <see cref="Guid"/> used by Visual Studio to represent the project flavor.</param>
        /// <returns>true, if the project has the specified flavor, otherwise false.</returns>
        /// <remarks>Implemented based on Microsoft.VSDesigner.Common.DTEUtil.ProjectHasTargetFlavor().</remarks>
        private bool ProjectHasTargetFlavor(Project project, Guid targetFlavor)
        {
            IVsHierarchy hierarchy = this.GetIVsHierarchy(project);
            if (hierarchy != null)
            {
                IVsAggregatableProject aggregatableProject = hierarchy as IVsAggregatableProject;
                if (aggregatableProject != null)
                {
                    string projectTypeGuids;
                    Marshal.ThrowExceptionForHR(aggregatableProject.GetAggregateProjectTypeGuids(out projectTypeGuids));
                    foreach (string projectFlavor in projectTypeGuids.Split(new char[] { ';' }))
                    {
                        if (!string.IsNullOrEmpty(projectFlavor))
                        {
                            if (targetFlavor.Equals(new Guid(projectFlavor)))
                            {
                                return true;
                            }
                        }
                    }
                }
                else if (targetFlavor.Equals(project.Kind))
                {
                    return true;
                }
            }

            return false;
        }

#if DEV10
        /// <summary>
        /// Temporarily suppresses Data Source Wizard.
        /// </summary>
        /// <remarks>
        /// This only works in Visual Studio 2010 and later versions.
        /// </remarks>
        private void SuppressDataSourceWizard()
        {
            using (var serviceProvider = new ServiceProvider(this.automationObject as Microsoft.VisualStudio.OLE.Interop.IServiceProvider))
            {
                IVsMonitorSelection service = (IVsMonitorSelection)serviceProvider.GetService(typeof(SVsShellMonitorSelection));
                Guid uiContext = new Guid("{5705AD15-40EE-4426-AD3E-BA750610D599}"); // Microsoft.VisualStudio.Shell.Interop.UIContextGuids100.DataSourceWizardSuppressed
                uint uiCookie;
                ErrorHandler.ThrowOnFailure(service.GetCmdUIContextCookie(ref uiContext, out uiCookie));
                ErrorHandler.ThrowOnFailure(service.SetCmdUIContext(uiCookie, 1));
            }
        }
#endif

        #endregion
    }
}
